/*
 * Copyright (C) 2012 Square, Inc.
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package okhttp3.internal.platform;

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Nullable;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;

import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.internal.tls.BasicCertificateChainCleaner;
import okhttp3.internal.tls.BasicTrustRootIndex;
import okhttp3.internal.tls.CertificateChainCleaner;
import okhttp3.internal.tls.TrustRootIndex;
import okio.Buffer;

/**
 * Access to platform-specific features.
 *
 * <h3>Server name indication (SNI)</h3>
 *
 * <p>
 * Supported on Android 2.3+.
 *
 * Supported on OpenJDK 7+
 *
 * <h3>Session Tickets</h3>
 *
 * <p>
 * Supported on Android 2.3+.
 *
 * <h3>Android Traffic Stats (Socket Tagging)</h3>
 *
 * <p>
 * Supported on Android 4.0+.
 *
 * <h3>ALPN (Application Layer Protocol Negotiation)</h3>
 *
 * <p>
 * Supported on Android 5.0+. The APIs were present in Android 4.4, but that
 * implementation was unstable.
 *
 * Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library).
 *
 * Supported on OpenJDK 9 via SSLParameters and SSLSocket features.
 *
 * <h3>Trust Manager Extraction</h3>
 *
 * <p>
 * Supported on Android 2.3+ and OpenJDK 7+. There are no public APIs to recover
 * the trust manager that was used to create an {@link SSLSocketFactory}.
 *
 * <h3>Android Cleartext Permit Detection</h3>
 *
 * <p>
 * Supported on Android 6.0+ via {@code NetworkSecurityPolicy}.
 */
public class Platform {
	private static final Platform PLATFORM = findPlatform();
	public static final int INFO = 4;
	public static final int WARN = 5;
	private static final Logger logger = Logger.getLogger(OkHttpClient.class.getName());

	public static Platform get() {
		return PLATFORM;
	}

	/** Prefix used on custom headers. */
	public String getPrefix() {
		return "OkHttp";
	}

	protected @Nullable X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
		// Attempt to get the trust manager from an OpenJDK socket factory. We attempt
		// this on all
		// platforms in order to support Robolectric, which mixes classes from both
		// Android and the
		// Oracle JDK. Note that we don't support HTTP/2 or other nice features on
		// Robolectric.
		try {
			Class<?> sslContextClass = Class.forName("sun.security.ssl.SSLContextImpl");
			Object context = readFieldOrNull(sslSocketFactory, sslContextClass, "context");
			if (context == null)
				return null;
			return readFieldOrNull(context, X509TrustManager.class, "trustManager");
		} catch (ClassNotFoundException e) {
			return null;
		}
	}

	/**
	 * Configure TLS extensions on {@code sslSocket} for {@code route}.
	 *
	 * @param hostname non-null for client-side handshakes; null for server-side
	 *                 handshakes.
	 */
	public void configureTlsExtensions(SSLSocket sslSocket, @Nullable String hostname, List<Protocol> protocols) {
	}

	/**
	 * Called after the TLS handshake to release resources allocated by
	 * {@link #configureTlsExtensions}.
	 */
	public void afterHandshake(SSLSocket sslSocket) {
	}

	/** Returns the negotiated protocol, or null if no protocol was negotiated. */
	public @Nullable String getSelectedProtocol(SSLSocket socket) {
		return null;
	}

	public void connectSocket(Socket socket, InetSocketAddress address, int connectTimeout) throws IOException {
		// 最终调用java的connect
		socket.connect(address, connectTimeout);
	}

	public void log(int level, String message, @Nullable Throwable t) {
		Level logLevel = level == WARN ? Level.WARNING : Level.INFO;
		logger.log(logLevel, message, t);
	}

	public boolean isCleartextTrafficPermitted(String hostname) {
		return true;
	}

	/**
	 * Returns an object that holds a stack trace created at the moment this method
	 * is executed. This should be used specifically for {@link java.io.Closeable}
	 * objects and in conjunction with {@link #logCloseableLeak(String, Object)}.
	 */
	public Object getStackTraceForCloseable(String closer) {
		if (logger.isLoggable(Level.FINE)) {
			return new Throwable(closer); // These are expensive to allocate.
		}
		return null;
	}

	public void logCloseableLeak(String message, Object stackTrace) {
		if (stackTrace == null) {
			message += " To see where this was allocated, set the OkHttpClient logger level to FINE: "
					+ "Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);";
		}
		log(WARN, message, (Throwable) stackTrace);
	}

	public static List<String> alpnProtocolNames(List<Protocol> protocols) {
		List<String> names = new ArrayList<>(protocols.size());
		for (int i = 0, size = protocols.size(); i < size; i++) {
			Protocol protocol = protocols.get(i);
			if (protocol == Protocol.HTTP_1_0)
				continue; // No HTTP/1.0 for ALPN.
			names.add(protocol.toString());
		}
		return names;
	}

	public CertificateChainCleaner buildCertificateChainCleaner(X509TrustManager trustManager) {
		return new BasicCertificateChainCleaner(buildTrustRootIndex(trustManager));
	}

	public CertificateChainCleaner buildCertificateChainCleaner(SSLSocketFactory sslSocketFactory) {
		X509TrustManager trustManager = trustManager(sslSocketFactory);

		if (trustManager == null) {
			throw new IllegalStateException("Unable to extract the trust manager on " + Platform.get()
					+ ", sslSocketFactory is " + sslSocketFactory.getClass());
		}

		return buildCertificateChainCleaner(trustManager);
	}

	public static boolean isConscryptPreferred() {
		// mainly to allow tests to run cleanly
		if ("conscrypt".equals(System.getProperty("okhttp.platform"))) {
			return true;
		}

		// check if Provider manually installed
		String preferredProvider = Security.getProviders()[0].getName();
		return "Conscrypt".equals(preferredProvider);
	}

	/** Attempt to match the host runtime to a capable Platform implementation. */
	private static Platform findPlatform() {
		Platform android = AndroidPlatform.buildIfSupported();

		if (android != null) {
			return android;
		}

		if (isConscryptPreferred()) {
			Platform conscrypt = ConscryptPlatform.buildIfSupported();

			if (conscrypt != null) {
				return conscrypt;
			}
		}

		Platform jdk9 = Jdk9Platform.buildIfSupported();

		if (jdk9 != null) {
			return jdk9;
		}

		Platform jdkWithJettyBoot = JdkWithJettyBootPlatform.buildIfSupported();

		if (jdkWithJettyBoot != null) {
			return jdkWithJettyBoot;
		}

		// Probably an Oracle JDK like OpenJDK.
		return new Platform();
	}

	/**
	 * Returns the concatenation of 8-bit, length prefixed protocol names.
	 * http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4
	 */
	static byte[] concatLengthPrefixed(List<Protocol> protocols) {
		Buffer result = new Buffer();
		for (int i = 0, size = protocols.size(); i < size; i++) {
			Protocol protocol = protocols.get(i);
			if (protocol == Protocol.HTTP_1_0)
				continue; // No HTTP/1.0 for ALPN.
			result.writeByte(protocol.toString().length());
			result.writeUtf8(protocol.toString());
		}
		return result.readByteArray();
	}

	static @Nullable <T> T readFieldOrNull(Object instance, Class<T> fieldType, String fieldName) {
		for (Class<?> c = instance.getClass(); c != Object.class; c = c.getSuperclass()) {
			try {
				Field field = c.getDeclaredField(fieldName);
				field.setAccessible(true);
				Object value = field.get(instance);
				if (value == null || !fieldType.isInstance(value))
					return null;
				return fieldType.cast(value);
			} catch (NoSuchFieldException ignored) {
			} catch (IllegalAccessException e) {
				throw new AssertionError();
			}
		}

		// Didn't find the field we wanted. As a last gasp attempt, try to find the
		// value on a delegate.
		if (!fieldName.equals("delegate")) {
			Object delegate = readFieldOrNull(instance, Object.class, "delegate");
			if (delegate != null)
				return readFieldOrNull(delegate, fieldType, fieldName);
		}

		return null;
	}

	public SSLContext getSSLContext() {
		String jvmVersion = System.getProperty("java.specification.version");
		if ("1.7".equals(jvmVersion)) {
			try {
				// JDK 1.7 (public version) only support > TLSv1 with named protocols
				return SSLContext.getInstance("TLSv1.2");
			} catch (NoSuchAlgorithmException e) {
				// fallback to TLS
			}
		}

		try {
			return SSLContext.getInstance("TLS");
		} catch (NoSuchAlgorithmException e) {
			throw new IllegalStateException("No TLS provider", e);
		}
	}

	public TrustRootIndex buildTrustRootIndex(X509TrustManager trustManager) {
		return new BasicTrustRootIndex(trustManager.getAcceptedIssuers());
	}

	public void configureSslSocketFactory(SSLSocketFactory socketFactory) {
	}

	@Override
	public String toString() {
		return getClass().getSimpleName();
	}
}
